iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 10

Day 10|Express 的中間人:Middleware 到底在做什麼?

  • 分享至 

  • xImage
  •  

前言

前面幾天,我們已經使用 Express 建立 Route,完成 Note API 的 CRUD,也把不同的 Route 拆分到 Router 中。現在 API 已經可以正常接收 Request、處理資料,再回傳 Response,不過在實際開發中,一個 Request 通常不會直接進入 Route Handler,而是會先經過一些額外的處理流程。

例如:

  • 希望每次 Request 進來時,都先記錄請求的 Method 和 URL
  • 需要登入的 API,要先確認使用者是否已經登入
  • 收到資料後,要先檢查格式是否正確
  • 如果請求的 API 不存在,要回傳 404 Not Found
  • 如果程式執行過程發生錯誤,要有統一的錯誤處理方式

這些工作雖然不是 API 本身的主要功能,卻可能需要在 Request 處理過程中執行。

Express 提供的 Middleware,就是用來處理這類共同或額外的流程。

Middleware 是什麼?

Middleware 可以理解成 Express Request 處理流程中的中間處理函式。Request 進入 Express 後,可以先經過一個或多個 Middleware,再繼續往後執行。

Middleware 通常會接收 reqresnext 三個參數:

  • req 是 Request,可以用來取得前端送來的請求資料
  • res 是 Response,可以用來回傳結果
  • next 則是用來把目前的處理交給下一個 Middleware 或 Route。
function middleware(req, res, next) {
  // ...
}

因此,Middleware 可以先處理自己的工作,再決定這個 Request 要不要繼續往下走。正常情況下,處理完成後會呼叫 next()

Request
   ↓
Middleware
   ↓
next()
   ↓
繼續往後處理

但如果 Middleware 已經可以直接結束這次 Request,就不需要呼叫 next(),例如驗證失敗時,可以直接回傳 401

function checkAuth(req, res, next) {
  const token = req.headers.authorization;

  if (!token) {
    return res.status(401).json({
      message: "請先登入"
    });
  }

  next();
}

這裡沒有 Token 時,Middleware 會直接回傳 Response,Request 就在這裡結束;有 Token 時,才會呼叫 next() 繼續往後處理。

Request 如何經過 Middleware?

了解 Middleware 的基本概念後,可以用 Logger 來看看實際的 Request 流程。假設我們希望每次 Request 進來時,都記錄 Method 和 URL,可以建立一個 Logger Middleware:

const express = require("express");

const app = express();

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();
}

app.use(logger);

app.get("/notes", (req, res) => {
  res.json([
    { id: 1, title: "第一篇筆記" },
    { id: 2, title: "第二篇筆記" }
  ]);
});

app.listen(3000);

當前端送出:

GET /notes

Request 會先進入 logger,這時 req.method 取得的是 GETreq.url 取得的是 /notes,所以 Console 會印出:

GET /notes

Logger 完成工作後呼叫 next(),Express 才會繼續往後尋找可以處理這個 Request 的 Route。找到 /notes 後,就會執行 Route Handler,最後回傳 Response。

Request
   ↓
logger
   ↓
next()
   ↓
/notes Route
   ↓
Response

這就是 Middleware 的基本用途。它可以在 Request 往後處理的過程中加入額外工作,而不需要把這些邏輯全部寫進 Route Handler。

Middleware 的執行順序

Middleware 會依照註冊的順序執行,所以放置的位置非常重要。例如:

app.use((req, res, next) => {
  console.log("Middleware 1");
  next();
});

app.use((req, res, next) => {
  console.log("Middleware 2");
  next();
});

app.get("/notes", (req, res) => {
  console.log("Route");
  res.json({ message: "Hello" });
});

當 Request 進入 /notes 時,會依序執行 Middleware 1Middleware 2 和 Route Handler,因此 Console 會看到:

Middleware 1
Middleware 2
Route

也就是說,如果某個 Middleware 必須先處理,就要放在需要它的 Route 前面。這個概念在 404 Middleware 特別重要,因為它需要等前面的 Route 都沒有處理 Request 後,才能接手。

Middleware 可以放在哪裡?

Middleware 要放在哪裡,主要取決於它需要被多少地方使用。如果整個 Application 都需要,就可以掛在 app 上;如果只有某一組 API 需要,就可以掛在 Router 上;如果只有特定 Route 需要,也可以直接放進 Route 的處理流程。

掛在 Application 上

如果 Middleware 希望整個 Application 都可以使用,可以透過 app.use() 註冊:

app.use(logger);

例如:

const express = require("express");

const app = express();

function logger(req, res, next) {
  console.log(`${req.method} ${req.url}`);
  next();
}

app.use(logger);

app.get("/notes", (req, res) => {
  res.json({ message: "Notes" });
});

app.get("/users", (req, res) => {
  res.json({ message: "Users" });
});

app.listen(3000);

這樣 /notes/users 等符合條件的 Request 都會先經過 logger。這種方式適合處理整個 Application 都可能需要的功能,例如 Logger 或 CORS。

掛在 Router 上

如果 Middleware 只需要套用在某一組 API,可以把它放在 Router 上。假設 Notes 相關 API 都需要先做登入檢查,可以使用 router.use()

const express = require("express");

const router = express.Router();

function checkAuth(req, res, next) {
  console.log("檢查登入狀態");
  next();
}

router.use(checkAuth);

router.get("/", (req, res) => {
  res.json({ message: "取得 Notes" });
});

router.post("/", (req, res) => {
  res.json({ message: "建立 Note" });
});

module.exports = router;

再由 app.js 掛載 Router:

const express = require("express");
const notesRouter = require("./routes/notes");

const app = express();

app.use("/notes", notesRouter);

app.listen(3000);

這樣進入 /notes Router 的 Request 都會先經過 checkAuth,其他 Router 則不會受到影響。

因此,app.use()router.use() 可以先理解成兩種不同的使用範圍:

app.use()
→ 整個 Application 共用

router.use()
→ 某一組 API 共用

放進 Route 的處理流程

有時候只有某一條 API 需要額外處理,就不需要把 Middleware 套用到整個 Router,也可以直接把多個 callback 放進 Route。

例如:

router.get("/", checkAuth, (req, res) => {
  res.json({
    message: "取得 Notes"
  });
});

這裡 checkAuth 會先執行,如果檢查成功並呼叫 next(),才會繼續執行後面的 Route Handler。也可以放入多個 callback:

router.post("/", checkAuth, validateNote, (req, res) => {
  res.json({
    message: "建立 Note"
  });
});

這些函式會按照順序執行,所以可以把需要先處理的工作放在 Route Handler 前面。這種寫法不需要另外理解成一種 Middleware 類型,只要知道 Route 可以接收多個 callback,讓不同的處理工作依序執行即可。

404 Middleware

Middleware 也可以用來處理沒有符合 Route 的 Request。假設現在只有 /notes 這個 Route:

app.get("/notes", (req, res) => {
  res.json({
    message: "取得 Notes"
  });
});

如果前端請求 /users,因為沒有任何 Route 可以處理這個 URL,Request 就會繼續往後走。這時可以在所有 Route 後面加入一個 Middleware:

app.use((req, res) => {
  res.status(404).json({
    message: "找不到此 API"
  });
});

這個 Middleware 會接住前面沒有被處理的 Request,因此 /users 最後就會得到 404 Not Found。它最重要的不是寫法,而是放置的位置,因為必須等前面的 Route 都沒有符合之後,才輪得到它處理。

Error-handling Middleware

除了找不到 Route,程式執行過程中也可能發生錯誤。這時候可以使用 Error-handling Middleware 集中處理:

app.use((err, req, res, next) => {
  console.error(err);

  res.status(500).json({
    message: "伺服器發生錯誤"
  });
});

這個 Middleware 和一般 Middleware 不同,它會接收四個參數:errreqresnext。當前面的程式發生錯誤時,可以透過 next(error) 把錯誤交給它處理:

app.get("/notes", (req, res, next) => {
  try {
    // 執行可能發生錯誤的程式
  } catch (error) {
    next(error);
  }
});

這樣錯誤就可以集中處理,不需要每個 Route 都自己處理相同的錯誤回應。

整體 Middleware 怎麼設計?

前面已經知道 Middleware 可以放在 Application、Router 或特定 Route 的處理流程中。實際開發時,通常會依照功能拆成不同的 Middleware,讓每個 Middleware 負責自己的工作。

例如專案可以規劃成:

project/
├── app.js
├── routes/
│   ├── notes.js
│   └── users.js
├── middlewares/
│   ├── logger.js
│   ├── auth.js
│   └── validateNote.js
└── package.json

其中 logger.js 負責記錄 Request,auth.js 負責驗證使用者,validateNote.js 負責檢查資料格式。

app.js 則負責整個 Application 的設定,例如註冊共用的 Middleware、掛載 Router,以及處理找不到 Route 和程式錯誤的情況:

const express = require("express");
const notesRouter = require("./routes/notes");
const logger = require("./middlewares/logger");

const app = express();

app.use(express.json());
app.use(logger);
app.use("/notes", notesRouter);

// 404 Middleware
app.use((req, res) => {
  res.status(404).json({
    message: "找不到此 API"
  });
});

// Error-handling Middleware
app.use((err, req, res, next) => {
  console.error(err);

  res.status(500).json({
    message: "伺服器發生錯誤"
  });
});

app.listen(3000);

整體架構可以簡單理解成:

Application
│
├── 共用 Middleware
│
├── Router
│   ├── Router Middleware
│   └── Route Handler
│
├── 404 Middleware
│
└── Error-handling Middleware

Middleware 會按照註冊順序往下執行,因此設計時除了考慮「誰需要使用」,也要考慮「什麼時候需要執行」。例如共用的 Middleware 通常會放在 Route 前面,404 Middleware 則要放在所有 Route 後面,這樣才能接住前面沒有處理的 Request。

所以設計 Middleware 時,可以先記住一個簡單原則:

需要共用的功能放外層,需要局部使用的功能放內層,需要最後處理的情況就放在後面。

這樣當 API 越來越多時,就可以把不同的處理工作分開管理,讓 Middleware 和 Route Handler 各自負責自己的功能。

小結

Middleware 可以把 Request 處理過程中的額外工作獨立出來,像是記錄 Request、驗證或資料檢查。實際使用時,先看這個功能需要被多少地方共用,再決定要放在 Application、Router,還是特定 Route 中。

當 API 越來越多時,這樣的設計可以減少重複程式碼,也讓每個 Route 的責任更加清楚。


上一篇
Day 09|API 越寫越多怎麼辦?用 Router 拆分路由
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言